Skip to content

feat: sync Client + sync Middleware/Retry/Bulkhead + RetryBudget thread-safety#31

Merged
lesnik512 merged 24 commits into
mainfrom
feat/sync-client
Jun 7, 2026
Merged

feat: sync Client + sync Middleware/Retry/Bulkhead + RetryBudget thread-safety#31
lesnik512 merged 24 commits into
mainfrom
feat/sync-client

Conversation

@lesnik512

Copy link
Copy Markdown
Member

Summary

  • Adds a fully-featured sync Client matching AsyncClient (typed decoding, middleware, Retry, Bulkhead, stream(), with/close() lifecycle, httpx2.Client injection).
  • Adds sync Middleware, Next, before_request/after_response/on_error decorators, sync compose.
  • Adds sync Retry (uses time.sleep) and sync Bulkhead (uses threading.Semaphore).
  • Makes RetryBudget thread-safe via an internal threading.Lock. Same class for both worlds; a single instance is safe to share across (sync Client, AsyncClient) pairs and across threads.
  • Extracts shared helpers (map_httpx2_exception, _raise_on_status_error, streaming-body predicates, STREAMING_BODY_MARKER) to _internal/exception_mapping.py and _internal/status.py.

Part 2 of 2. Part 1 (rename, PR #30) is already merged. Cut one combined release after this lands.

Source spec: planning/specs/2026-06-07-sync-client-design.md. Plan: planning/plans/2026-06-07-sync-client-plan.md (Tasks B1–B25). Release notes drafted at planning/releases/0.8.0.md.

Test plan

  • just lint clean
  • just test all green — 371 tests, 100% line coverage
  • Sync test suite mirrors async — test_client_sync.py, test_retry_sync.py, test_bulkhead_sync.py, test_middleware_sync.py, test_client_stream_sync.py
  • test_retry_budget_threadsafety.py proves the lock keeps the deques consistent under concurrent deposit/withdraw
  • test_threading_with_shared_budget.py proves a single RetryBudget works under mixed sync-threads + async-loop pressure
  • test_optional_extras_isolation.py still green — sync addition introduces no new transitive imports
  • mkdocs build --strict clean

Release notes

Draft at planning/releases/0.8.0.md. The version decision (0.8.0 vs 1.0.0) can be made at tag time.

🤖 Generated with Claude Code

lesnik512 and others added 24 commits June 7, 2026 19:36
Sync Client (landing in this PR) may share a Retry/RetryBudget across
a ThreadPoolExecutor. The token-bucket deques would race without a lock.
Uncontended lock cost in CPython is ~50-100ns per op — negligible vs
HTTP latency. Existing async paths are unaffected; lock is taken
unconditionally for one type, one mental model, no flag at the call
site.
…on_mapping.py

Shared by AsyncClient (and the upcoming Client). Pure function; the
existing _httpx2_exception_mapper context manager in client.py will
delegate to this in Task B5.
…to _internal/status.py

Both worlds share the status-code dispatch (already pure sync) and the
STREAMING_BODY_MARKER. Predicates split: _is_streaming_body_async checks
__aiter__; _is_streaming_body_sync checks __iter__ with list/tuple in the
safe-list (common in sync code, never streaming).
…helpers

Pulls map_httpx2_exception, _raise_on_status_error, _is_streaming_body_*,
and STREAMING_BODY_MARKER out of client.py into _internal/. Behavior
unchanged; sets up Client (sync) to share the same dispatch.
Same algorithm: budget deposit per attempt, status/network/timeout
gates, idempotent-method check, streaming-body refusal, Retry-After,
full-jitter backoff, budget refusal, PEP-678 add_note on giving up.
Uses time.sleep for the delay; no attempt_timeout (removed from both
worlds in PR 1). Shares the same observability emitters (httpware.retry
logger, retry.streaming_refused / retry.giving_up / retry.budget_refused
events).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
HTTP methods land in the next commit; stream() after.
Reword _HTTPX2_CLIENT_CONFLICT_MESSAGE and _DEFAULT_DECODER_MISSING_MESSAGE to be world-neutral since they are now shared between Client and AsyncClient.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…/head/options/request/send)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…+ decorators

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@lesnik512 lesnik512 self-assigned this Jun 7, 2026
@lesnik512 lesnik512 merged commit d2c24f2 into main Jun 7, 2026
5 checks passed
@lesnik512 lesnik512 deleted the feat/sync-client branch June 7, 2026 17:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant